<?php
/**
* Authorization component class file.
*
* @author Christoffer Niska <cniska@live.com>
* @copyright Copyright &copy; 2010 Christoffer Niska
* @since 0.5
*/
class Authorization extends CApplicationComponent
{
	/**
	* @var CDbAuthManager Auth manager
	*/
	private $_authManager;

	/**
	* @var array Role hierarchy.
	*/
	private $_hierarchy;

	/**
	* @var array Map of Auth items with role hierarchy position as key.
	*/
	private $_authChildMap;

	/**
	* @var string Name of the topmost role in the hierarchy
	*/
	private $_superUser;

	/**
	* @var array Valid authorization item types
	*/
	private $_authItemTypeList = array('operation', 'task', 'role');

	/**
	* Initialization.
	*/
	public function init()
	{
		$this->_authManager = Yii::app()->getAuthManager();
	}

	/**
	* Sets the roles hierarchy.
	* @param string $superUser Name of the topmost role in the hierarchy
	*/
	public function createHierarchy($superUser=NULL)
	{
		if( isset($superUser)===true )
			$this->_superUser = $superUser;

		$role = $this->_authManager->getAuthItem($this->_superUser);
		$this->_getHierarchyRecursive($role);
	}

	/**
	* Gets the role hierarchy recursively.
	* @param CAuthItem $role Role
	*/
	private function _getHierarchyRecursive(CAuthItem $role)
	{
		// Add the role to the hierarchy
		$this->_hierarchy[] = $role;

		foreach( $role->children as $name => $child )
			if( $this->typeIsRole($child->type)===true )
				$this->_getHierarchyRecursive($child);
	}

	/**
	* Creates a new role.
	* @param string $name Role name
	* @param string $description Description
	* @param string $bizRule Business rule
	* @param string $data Non-serialized data
	* @return CAuthItem
	*/
	public function createRole($name, $description='', $bizRule=NULL, $data=NULL)
	{
		// Calculate the last position in the role hierarchy
		$lastPosition = count($this->_hierarchy)-1;

		$role->data = $data!=='' ? $this->saferEval('return '.$data.';') : NULL;

		// Get the necessary roles
		$parentRole = $this->_hierarchy[ $lastPosition-1 ];
		$childRole = $this->_hierarchy[ $lastPosition ];
		$newRole = $this->_authManager->createAuthItem($name, CAuthItem::TYPE_ROLE, $description, $bizRule, $data);

		// Reorganize the roles
		$parentRole->removeChild($childRole->name);
		$parentRole->addChild($newRole->name);
		$newRole->addChild($childRole->name);

		// Rebuild the role hierarchy
		$this->createHierarchy();
	}

	/**
	* Updates an existing role.
	* @param string $oldName Old role name
	* @param string $name New role name
	* @param string $description Description
	* @param string $bizRule Business rule
	* @param string $data Non-serialized data
	* @return CAuthItem
	*/
	public function updateRole($oldName, $name, $description, $bizRule, $data)
	{
		// Calculate the role position in the role hierarchy
		$position = $this->_getRolePosition($oldName);

		// Get the role
		$role = $this->_hierarchy[ $position ];

		// Set name and description
		$role->name = $name;
		$role->description = $description!=='' ? $description : NULL;
		$role->bizRule = $bizRule!=='' ? $bizRule : NULL;

		// Make sure that data is not already serialized
		if( @unserialize($data)===false )
			$role->data = $data!=='' ? $this->saferEval('return '.$data.';') : NULL;

		// Make sure that this is not the topmost role
		if( isset($this->_hierarchy[ $position-1 ])===true )
		{
			$parentRole = $this->_hierarchy[ $position-1 ];
			$parentRole->removeChild($oldName);
			$parentRole->addChild($name);
		}

		// Save the role
		$this->_authManager->saveAuthItem($role, $role->name);

		// Role name has changed
		if( $oldName!==$name )
		{
			// Copy all children from the old role to the new one
			foreach( $this->_authManager->getItemChildren($oldName) as $childName => $authChild )
			{
				$this->_authManager->removeItemChild($oldName, $childName);
				$this->_authManager->addItemChild($name, $childName);
			}
		}

		// Rebuild the role hierarchy
		$this->createHierarchy();

		// Return the role
		return $role;
	}

	/**
	* Deletes a role.
	* @param string $name Role name
	* @return bool
	*/
	public function deleteRole($name)
	{
		// Calculate the last position in the role hierarchy
		$position = $this->_getRolePosition($name);

		// Get the necessary roles
		$role = $this->_hierarchy[ $position ];
		$parentRole = $this->_hierarchy[ $position-1 ];
		$childRole = $this->_hierarchy[ $position+1 ];

		// Remove all children for the role
		foreach( $role->children as $childName => $authChild )
			$role->removeChild($childName);

		// Reorganize the roles
		$parentRole->removeChild($role->name);
		$role->removeChild($childRole->name);
		$parentRole->addChild($childRole->name);

		// Delete the role
		if( $this->_authManager->removeAuthItem($role->name)!==false )
		{
			// Rebuild the role hierarchy
			$this->createHierarchy();
			return true;
		}

		// Role could not be deleted
		return false;
	}

	/**
	* Promotes a role.
	* @param string $name Name of the role to promote
	*/
	public function promoteRole($name)
	{
		// Get the roles position in the hierarchy
		$position = $this->_getRolePosition($name);

		// Role is not at the top of the hierarchy
		if( $position>1 )
		{
			// Get the necessary roles
			$role = $this->_hierarchy[ $position ];
			$grandParentRole = $this->_hierarchy[ $position-2 ];
			$parentRole = $this->_hierarchy[ $position-1 ];
			$childRole = $this->_hierarchy[ $position+1 ];

			// Reorganize the roles
			$role->removeChild($childRole->name);
			$parentRole->removeChild($role->name);
			$grandParentRole->removeChild($parentRole->name);
			$grandParentRole->addChild($role->name);
			$parentRole->addChild($childRole->name);
			$role->addChild($parentRole->name);

			// Rebuild the role hierarchy
			$this->createHierarchy();
		}
	}

	/**
	* Demotes a role.
	* @param string $name Name of the role to demote
	*/
	public function demoteRole($name)
	{
		// Get the roles position in the hierarchy
		$position = $this->_getRolePosition($name);

		// Role is not at the bottom of the hierarchy
		if( $position<(count($this->_hierarchy)-2) )
		{
			// Get the necessary roles
			$role = $this->_hierarchy[ $position ];
			$parentRole = $this->_hierarchy[ $position-1 ];
			$childRole = $this->_hierarchy[ $position+1 ];
			$grandChildRole = $this->_hierarchy[ $position+2 ];

			// Reorganize the roles
			$childRole->removeChild($grandChildRole->name);
			$role->removeChild($childRole->name);
			$parentRole->removeChild($role->name);
			$parentRole->addChild($childRole->name);
			$role->addChild($grandChildRole->name);
			$childRole->addChild($role->name);

			// Rebuild the role hierarchy
			$this->createHierarchy();
		}
	}

	/**
	* Gets the roles position in the hierarchy.
	* @param string $name Name of the role to get the position for
	* @return integer Hierarchy position
	*/
	private function _getRolePosition($name)
	{
		foreach( $this->_hierarchy as $position => $role )
			if( $role->name==$name )
				return (int)$position; // Role found

		return false;
	}

	/**
	* Gets the role assigned to a specific user.
	* @param integer $userId User id
	* @return string Role name or null if no roles are assigned
	*/
	public function getAssignedRole($userId)
	{
		foreach( $this->_hierarchy as $role )
			if( $this->_authManager->isAssigned($role->name, $userId)===true )
				return $role->name; // Assigned role found

		return NULL;
	}

	/**
	* Assigns the given role to a specific user
	* revoking the current role if necessary.
	* @param string $name Role name
	* @param integer $userId User id
	* @param string $bizRule Business rule
	* @param string $data Data
	* @return CAuthAssignment
	*/
	public function assignRole($name, $userId, $bizRule=NULL, $data=NULL)
	{
		if( ($assignedRoleName = $this->getAssignedRole($userId))!==NULL )
			$this->revokeRole($assignedRoleName, $userId);

		return $this->_authManager->assign($name, $userId, $bizRule, $data);
	}

	/**
	* Revokes the given role from a specific user.
	* @param string $name Role name
	* @param integer $userId User id
	* @return boolean
	*/
	public function revokeRole($name, $userId)
	{
		return $this->_authManager->revoke($name, $userId);
	}

	/**
	* Gets a role by its name.
	* @param string $name Role name
	* @return CAuthItem
	*/
	public function getRoleByName($name)
	{
		return $this->_hierarchy[ $this->_getRolePosition($name) ];
	}

	/**
	* Sets the auth item map.
	* @return array Auth item map; roleName => authChildList
	*/
	public function createAuthChildMap()
	{
		$hierarchy = $this->_hierarchy;

		// Remove the topmost role because it is the super user
		// that always has all rights
		unset($hierarchy[0]);

		if( count($hierarchy)>0 )
			foreach( $hierarchy as $role )
				$this->_authChildMap[ $role->name ] = $this->getAuthItemChildren($role);
	}

	/**
	* Gets the auth item children recursively.
	* @param CAuthItem $item Auth item to get children for
	* @return array Authorization item children
	*/
	public function getAuthItemChildren(CAuthItem $item)
	{
		$authItemChildren = array();

		foreach( $item->children as $name => $authItem )
		{
			$authItemChildren[ $name ] = $authItem;

			if( $this->typeIsRole($authItem->type)===true )
				if( count($grandChildren = $this->getAuthItemChildren($authItem))>0 )
					$authItemChildren = $this->mergeAuthItems($authItemChildren, $grandChildren);
		}

		return $authItemChildren;
	}

	/**
	* Creates an authorization item.
	* @param string $name Name
	* @param integer $type Type
	* @param string $description Description
	* @param string $bizRule Business rule
	* @param string $data Non-serialized data
	* @return CAuthItem
	*/
	public function createAuthItem($name, $type, $description='', $bizRule=NULL, $data=NULL)
	{
		if( isset($data)===true )
			$data = empty($data)===false ? $this->saferEval('return '.$data.';') : NULL;

		return $this->_authManager->createAuthItem($name, $type, $description, $bizRule, $data);
	}

	/**
	* Updates an authorization item.
	* @param string $oldName Old name
	* @param string $name Name
	* @param string $description Description
	* @param string $bizRule Business rule
	* @param string $data Non-serialized data
	*/
	public function updateAuthItem($oldName, $name, $description='', $bizRule=NULL, $data=NULL)
	{
		// Get the authorization item
		$authItem = $this->_authManager->getAuthItem($oldName);

		// Set name and description
		$authItem->name = $name;
		$authItem->description = $description!=='' ? $description : NULL;
		$authItem->bizRule = $bizRule!=='' ? $bizRule : NULL;

		// Make sure that data is not already serialized
		if( @unserialize($data)===false )
			$authItem->data = $data!=='' ? $this->saferEval('return '.$data.';') : NULL;

		// Save the authorization item
		$this->_authManager->saveAuthItem($authItem, $oldName);

		// Re-assign the authorization item with the new name
		foreach( $this->_hierarchy as $role )
		{
			if( $role->hasChild($oldName)===true )
			{
				$role->removeChild($oldName);
				$role->addChild($name);
				break;
			}
		}
	}

	/**
	* Deletes an authorization item.
	* @param string $name Authorization item name
	* @return boolean
	*/
	public function deleteAuthItem($name)
	{
		return $this->_authManager->removeAuthItem($name);
	}

	/**
	* Gets authorization items of given type(s).
	* @param string|array $type Type of auth items to get - valid types are 'operation', 'task' and 'role'
	* @return array Authorization items
	*/
	public function getAuthItems($type=NULL)
	{
		// Type is not given, set type to all valid types
		if( isset($type)===false )
			$type = $this->_authItemTypeList;

		// Type is not an array, convert it into one
		if( is_array($type)===false )
			$type = array($type);

		$items = array();
		foreach( $type as $t )
			$items[] = $this->_authManager->getAuthItems( $this->getAuthItemTypeByString($t) );

		$authItems = array();
		foreach( $items as $ai )
			$authItems = $this->mergeAuthItems($authItems, $ai);

		return $authItems;
	}

	/**
	* Merges two arrays with authorization items preserving the keys.
	* @param array $array1 Authorization item list
	* @param array $array2 Authorization item list
	* @return array Merged list
	*/
	public function mergeAuthItems($array1, $array2)
	{
		foreach( $array2 as $name => $authItem )
			if( isset($array1[ $name ])===false )
				$array1[ $name ] = $authItem;

		return $array1;
	}

	/**
	* Checks if an item exists in the auth item list.
	* @param array $authItemList List of auth items
	* @param string $itemName Name of the item to check for
	* @return bool True if found, else false
	*/
	public function authItemIsAssigned($name)
	{
		foreach( $this->_authChildMap as $roleName => $itemList )
			if( isset($itemList[ $name ])===true )
				return true; // Authorization item found in the child map

		return false;
	}

	/**
	* Adds a child to the given authorization item
	* @param string $name Authorization item name
	* @param string $child Name of the child item to addd
	*/
	public function addAuthItemChild($name, $child)
	{
		if( $this->_authManager->hasItemChild($name, $child)===false )
			$this->_authManager->addItemChild($name, $child);

		// TODO: Add error message if already has child.

		// Rebuild the role hierarchy and the authorization child map
		$this->createHierarchy();
		$this->createAuthChildMap();
	}

	/**
	* Removes a child from the given authorization item
	* @param string $name Authorization item name
	* @param string $child Name of the child item to remove
	*/
	public function removeAuthItemChild($name, $child)
	{
		// Make sure that the authorization item has the given child
		// and if so, remove it
		if( $this->_authManager->hasItemChild($name, $child)===true )
			$this->_authManager->removeItemChild($name, $child);

		// Rebuild the role hierarchy and the authorization child map
		$this->createHierarchy();
		$this->createAuthChildMap();
	}

	/**
	* Gets the authorization item type by string.
	* @param string $string Authorization item name
	* @return int Authorization item type
	*/
	public function getAuthItemTypeByString($string)
	{
		switch( $string )
		{
			case 'operation': return CAuthItem::TYPE_OPERATION;
			case 'task': return CAuthItem::TYPE_TASK;
			case 'role': return CAuthItem::TYPE_ROLE;
			default: throw new CException('Invalid authorization item type.');
		}
	}

	/**
	* Gets the authorization item type as a string.
	* @param int Authorization item type as integer
	* @return string Authorization item type
	*/
	public function getAuthItemTypeString($type)
	{
		switch( (int)$type )
		{
			case CAuthItem::TYPE_OPERATION: return Yii::t('auth', 'operation');
			case CAuthItem::TYPE_TASK: return Yii::t('auth', 'task');
			case CAuthItem::TYPE_ROLE: return Yii::t('auth', 'role');
			default: throw new CException('Invalid authorization item type.');
		}
	}

	/**
	* Checks if the given authorization item is an operation.
	* @param CAuthItem $item Authorization item
	* @return bool
	*/
	public function typeIsOperation($type)
	{
		return (int)$type===CAuthItem::TYPE_OPERATION;
	}

	/**
	* Checks if the given authorization item is a task.
	* @param CAuthItem $item Authorization item
	* @return bool
	*/
	public function typeIsTask($type)
	{
		return (int)$type===CAuthItem::TYPE_TASK;
	}

	/**
	* Checks if the given authorization item is a role.
	* @param CAuthItem $item Authorization item
	* @return bool
	*/
	public function typeIsRole($type)
	{
		return (int)$type===CAuthItem::TYPE_ROLE;
	}

	/**
	* Gets the role select options.
	* @return array Role select options
	*/
	public function getRoleSelectOptions()
	{
		$selectOptions = array();
		foreach( $this->_hierarchy as $role )
			$selectOptions[ $role->name ] = ucfirst($role->name);

		return $selectOptions;
	}

	/**
	* Gets the task select options.
	* @return array Task select options
	*/
	public function getTaskSelectOptions()
	{
		$taskList = $this->getAuthItems('task');

		$selectOptions = array();
		foreach( $taskList as $task )
			$selectOptions[ $task->name ] = ucfirst($task->name);

		return $selectOptions;
	}

	/**
	* Makes code safer for use with eval().
	* @param string $code Code to be run with eval.
	* @return mixed Eval'ed code or null if the code was not safe
	*/
	public function saferEval($code)
	{
		// Language consturcts
		$languageConstructs = array(
			'echo',
			'empty',
			'isset',
			'unset',
			'exit',
			'die',
			'include',
			'include_once',
			'require',
			'require_once',
		);

		// Loop through the language constructs
		foreach( $languageConstructs as $i )
			if( preg_match('/'.$i.'\ *\(?\ *[\"\']+/', $code)>0 )
				return NULL; // Language construct found, not safe for eval

		// Get a list of all defined functions
		$definedFunctions = get_defined_functions();
		$functions = $definedFunctions['internal']; //array_merge($definedFunctions['internal'], $definedFunctions['user']);

		// Loop through the functions and check the code for function calls
		// Append a '(' to the functions to avoid confusion between e.g. array() and array_merge()
		foreach( $functions as $f )
			if( preg_match('/'.$f.'\ *\({1}/', $code)>0 )
				return NULL; // Function call found, not safe for eval

		// Eval the safer code
		$evaledCode = @eval($code);

		// Return the eval'ed code or null if the result was false
		return $evaledCode!==false ? $evaledCode : NULL;
	}

	/**
	* @return CDbAuthManager Auth manager
	*/
	public function getAuthManager()
	{
		return $this->_authManager;
	}

	/**
	* Sets the Auth manager.
	* @param CDbAuthManager $authManager Auth manager
	*/
	public function setAuthManager($authManager)
	{
		$this->_authManager = $authManager;
	}

	/**
	* @return array Role hierarchy
	*/
	public function getHierarchy()
	{
		return $this->_hierarchy;
	}

	/**
	* Sets the role hierarchy.
	* @param array $hierarchy Role hierarchy
	*/
	public function setHierarchy($hierarchy)
	{
		$this->_hierarchy = $hierarchy;
	}

	/**
	* @return array Auth item map
	*/
	public function getAuthChildMap()
	{
		return $this->_authChildMap;
	}

	/**
	* Sets the Auth child map.
	* @param array $authChildMap Auth child map
	*/
	public function setAuthChildMap($authChildMap)
	{
		$this->_authChildMap = $authChildMap;
	}

	/**
	* @return string Super user name
	*/
	public function getSuperUser()
	{
		return $this->_superUser;
	}

	/**
	* Sets the super user name.
	* @param string $superUser Super user name
	*/
	public function setSuperUser($superUser)
	{
		$this->_superUser = $superUser;
	}
}
